// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/proxy/dhcp_proxy_script_fetcher_win.h"

#include <memory>
#include <vector>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/free_deleter.h"
#include "base/profiler/scoped_tracker.h"
#include "base/threading/sequenced_worker_pool.h"
#include "net/base/net_errors.h"
#include "net/proxy/dhcp_proxy_script_adapter_fetcher_win.h"

#include <iphlpapi.h>
#include <winsock2.h>

namespace {

// How many threads to use at maximum to do DHCP lookups. This is
// chosen based on the following UMA data:
// - When OnWaitTimer fires, ~99.8% of users have 6 or fewer network
//   adapters enabled for DHCP in total.
// - At the same measurement point, ~99.7% of users have 3 or fewer pending
//   DHCP adapter lookups.
// - There is however a very long and thin tail of users who have
//   systems reporting up to 100+ adapters (this must be some very weird
//   OS bug (?), probably the cause of http://crbug.com/240034).
//
// The maximum number of threads is chosen such that even systems that
// report a huge number of network adapters should not run out of
// memory from this number of threads, while giving a good chance of
// getting back results for any responsive adapters.
//
// The ~99.8% of systems that have 6 or fewer network adapters will
// not grow the thread pool to its maximum size (rather, they will
// grow it to 6 or fewer threads) so setting the limit lower would not
// improve performance or memory usage on those systems.
const int kMaxDhcpLookupThreads = 12;

// How long to wait at maximum after we get results (a PAC file or
// knowledge that no PAC file is configured) from whichever network
// adapter finishes first.
const int kMaxWaitAfterFirstResultMs = 400;

} // namespace

namespace net {

DhcpProxyScriptFetcherWin::DhcpProxyScriptFetcherWin(
    URLRequestContext* url_request_context)
    : state_(STATE_START)
    , num_pending_fetchers_(0)
    , destination_string_(NULL)
    , url_request_context_(url_request_context)
{
    DCHECK(url_request_context_);

    worker_pool_ = new base::SequencedWorkerPool(kMaxDhcpLookupThreads,
        "PacDhcpLookup");
}

DhcpProxyScriptFetcherWin::~DhcpProxyScriptFetcherWin()
{
    // Count as user-initiated if we are not yet in STATE_DONE.
    Cancel();

    worker_pool_->Shutdown();
}

int DhcpProxyScriptFetcherWin::Fetch(base::string16* utf16_text,
    const CompletionCallback& callback)
{
    // TODO(joi): Remove ScopedTracker below once crbug.com/476182 is fixed.
    tracked_objects::ScopedTracker tracking_profile1(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "476182 DhcpProxyScriptFetcherWin::Fetch 1"));

    DCHECK(CalledOnValidThread());
    if (state_ != STATE_START && state_ != STATE_DONE) {
        NOTREACHED();
        return ERR_UNEXPECTED;
    }

    state_ = STATE_WAIT_ADAPTERS;
    callback_ = callback;
    destination_string_ = utf16_text;

    last_query_ = ImplCreateAdapterQuery();
    // TODO(joi): Remove ScopedTracker below once crbug.com/476182 is fixed.
    tracked_objects::ScopedTracker tracking_profile2(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "476182 DhcpProxyScriptFetcherWin::Fetch 2"));
    GetTaskRunner()->PostTaskAndReply(
        FROM_HERE,
        base::Bind(
            &DhcpProxyScriptFetcherWin::AdapterQuery::GetCandidateAdapterNames,
            last_query_.get()),
        base::Bind(
            &DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone,
            AsWeakPtr(),
            last_query_));

    return ERR_IO_PENDING;
}

void DhcpProxyScriptFetcherWin::Cancel()
{
    DCHECK(CalledOnValidThread());

    CancelImpl();
}

void DhcpProxyScriptFetcherWin::CancelImpl()
{
    DCHECK(CalledOnValidThread());

    if (state_ != STATE_DONE) {
        callback_.Reset();
        wait_timer_.Stop();
        state_ = STATE_DONE;

        for (FetcherVector::iterator it = fetchers_.begin();
             it != fetchers_.end();
             ++it) {
            (*it)->Cancel();
        }

        fetchers_.clear();
    }
}

void DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone(
    scoped_refptr<AdapterQuery> query)
{
    // TODO(joi): Remove ScopedTracker below once crbug.com/476182 is fixed.
    tracked_objects::ScopedTracker tracking_profile1(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "476182 "
            "DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone 1"));

    DCHECK(CalledOnValidThread());

    // This can happen if this object is reused for multiple queries,
    // and a previous query was cancelled before it completed.
    if (query.get() != last_query_.get())
        return;
    last_query_ = NULL;

    // Enable unit tests to wait for this to happen; in production this function
    // call is a no-op.
    ImplOnGetCandidateAdapterNamesDone();

    // We may have been cancelled.
    if (state_ != STATE_WAIT_ADAPTERS)
        return;

    state_ = STATE_NO_RESULTS;

    // TODO(joi): Remove ScopedTracker below once crbug.com/476182 is fixed.
    tracked_objects::ScopedTracker tracking_profile2(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "476182 "
            "DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone 2"));

    const std::set<std::string>& adapter_names = query->adapter_names();

    if (adapter_names.empty()) {
        TransitionToDone();
        return;
    }

    // TODO(joi): Remove ScopedTracker below once crbug.com/476182 is fixed.
    tracked_objects::ScopedTracker tracking_profile3(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "476182 "
            "DhcpProxyScriptFetcherWin::OnGetCandidateAdapterNamesDone 3"));

    for (std::set<std::string>::const_iterator it = adapter_names.begin();
         it != adapter_names.end();
         ++it) {
        std::unique_ptr<DhcpProxyScriptAdapterFetcher> fetcher(
            ImplCreateAdapterFetcher());
        fetcher->Fetch(
            *it, base::Bind(&DhcpProxyScriptFetcherWin::OnFetcherDone, base::Unretained(this)));
        fetchers_.push_back(std::move(fetcher));
    }
    num_pending_fetchers_ = fetchers_.size();
}

std::string DhcpProxyScriptFetcherWin::GetFetcherName() const
{
    DCHECK(CalledOnValidThread());
    return "win";
}

const GURL& DhcpProxyScriptFetcherWin::GetPacURL() const
{
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(state_, STATE_DONE);

    return pac_url_;
}

void DhcpProxyScriptFetcherWin::OnFetcherDone(int result)
{
    DCHECK(state_ == STATE_NO_RESULTS || state_ == STATE_SOME_RESULTS);

    if (--num_pending_fetchers_ == 0) {
        TransitionToDone();
        return;
    }

    // If the only pending adapters are those less preferred than one
    // with a valid PAC script, we do not need to wait any longer.
    for (FetcherVector::iterator it = fetchers_.begin();
         it != fetchers_.end();
         ++it) {
        bool did_finish = (*it)->DidFinish();
        int result = (*it)->GetResult();
        if (did_finish && result == OK) {
            TransitionToDone();
            return;
        }
        if (!did_finish || result != ERR_PAC_NOT_IN_DHCP) {
            break;
        }
    }

    // Once we have a single result, we set a maximum on how long to wait
    // for the rest of the results.
    if (state_ == STATE_NO_RESULTS) {
        state_ = STATE_SOME_RESULTS;
        wait_timer_.Start(FROM_HERE,
            ImplGetMaxWait(), this, &DhcpProxyScriptFetcherWin::OnWaitTimer);
    }
}

void DhcpProxyScriptFetcherWin::OnWaitTimer()
{
    DCHECK_EQ(state_, STATE_SOME_RESULTS);

    TransitionToDone();
}

void DhcpProxyScriptFetcherWin::TransitionToDone()
{
    DCHECK(state_ == STATE_NO_RESULTS || state_ == STATE_SOME_RESULTS);

    int result = ERR_PAC_NOT_IN_DHCP; // Default if no fetchers.
    if (!fetchers_.empty()) {
        // Scan twice for the result; once through the whole list for success,
        // then if no success, return result for most preferred network adapter,
        // preferring "real" network errors to the ERR_PAC_NOT_IN_DHCP error.
        // Default to ERR_ABORTED if no fetcher completed.
        result = ERR_ABORTED;
        for (FetcherVector::iterator it = fetchers_.begin();
             it != fetchers_.end();
             ++it) {
            if ((*it)->DidFinish() && (*it)->GetResult() == OK) {
                result = OK;
                *destination_string_ = (*it)->GetPacScript();
                pac_url_ = (*it)->GetPacURL();
                break;
            }
        }
        if (result != OK) {
            destination_string_->clear();
            for (FetcherVector::iterator it = fetchers_.begin();
                 it != fetchers_.end();
                 ++it) {
                if ((*it)->DidFinish()) {
                    result = (*it)->GetResult();
                    if (result != ERR_PAC_NOT_IN_DHCP) {
                        break;
                    }
                }
            }
        }
    }

    CompletionCallback callback = callback_;
    CancelImpl();
    DCHECK_EQ(state_, STATE_DONE);
    DCHECK(fetchers_.empty());
    DCHECK(callback_.is_null()); // Invariant of data.

    // We may be deleted re-entrantly within this outcall.
    callback.Run(result);
}

int DhcpProxyScriptFetcherWin::num_pending_fetchers() const
{
    return num_pending_fetchers_;
}

URLRequestContext* DhcpProxyScriptFetcherWin::url_request_context() const
{
    return url_request_context_;
}

scoped_refptr<base::TaskRunner> DhcpProxyScriptFetcherWin::GetTaskRunner()
{
    return worker_pool_->GetTaskRunnerWithShutdownBehavior(
        base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN);
}

DhcpProxyScriptAdapterFetcher*
DhcpProxyScriptFetcherWin::ImplCreateAdapterFetcher()
{
    return new DhcpProxyScriptAdapterFetcher(url_request_context_,
        GetTaskRunner());
}

DhcpProxyScriptFetcherWin::AdapterQuery*
DhcpProxyScriptFetcherWin::ImplCreateAdapterQuery()
{
    return new AdapterQuery();
}

base::TimeDelta DhcpProxyScriptFetcherWin::ImplGetMaxWait()
{
    return base::TimeDelta::FromMilliseconds(kMaxWaitAfterFirstResultMs);
}

bool DhcpProxyScriptFetcherWin::GetCandidateAdapterNames(
    std::set<std::string>* adapter_names)
{
    DCHECK(adapter_names);
    adapter_names->clear();

    // The GetAdaptersAddresses MSDN page recommends using a size of 15000 to
    // avoid reallocation.
    ULONG adapters_size = 15000;
    std::unique_ptr<IP_ADAPTER_ADDRESSES, base::FreeDeleter> adapters;
    ULONG error = ERROR_SUCCESS;
    int num_tries = 0;

    do {
        adapters.reset(static_cast<IP_ADAPTER_ADDRESSES*>(malloc(adapters_size)));
        // Return only unicast addresses, and skip information we do not need.
        error = GetAdaptersAddresses(AF_UNSPEC,
            GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME,
            NULL,
            adapters.get(),
            &adapters_size);
        ++num_tries;
    } while (error == ERROR_BUFFER_OVERFLOW && num_tries <= 3);

    if (error == ERROR_NO_DATA) {
        // There are no adapters that we care about.
        return true;
    }

    if (error != ERROR_SUCCESS) {
        LOG(WARNING) << "Unexpected error retrieving WPAD configuration from DHCP.";
        return false;
    }

    IP_ADAPTER_ADDRESSES* adapter = NULL;
    for (adapter = adapters.get(); adapter; adapter = adapter->Next) {
        if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK)
            continue;
        if ((adapter->Flags & IP_ADAPTER_DHCP_ENABLED) == 0)
            continue;

        DCHECK(adapter->AdapterName);
        adapter_names->insert(adapter->AdapterName);
    }

    return true;
}

DhcpProxyScriptFetcherWin::AdapterQuery::AdapterQuery()
{
}

void DhcpProxyScriptFetcherWin::AdapterQuery::GetCandidateAdapterNames()
{
    ImplGetCandidateAdapterNames(&adapter_names_);
}

const std::set<std::string>&
DhcpProxyScriptFetcherWin::AdapterQuery::adapter_names() const
{
    return adapter_names_;
}

bool DhcpProxyScriptFetcherWin::AdapterQuery::ImplGetCandidateAdapterNames(
    std::set<std::string>* adapter_names)
{
    return DhcpProxyScriptFetcherWin::GetCandidateAdapterNames(adapter_names);
}

DhcpProxyScriptFetcherWin::AdapterQuery::~AdapterQuery()
{
}

} // namespace net
